﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using SimpleModeler.Graphics;
using System.Drawing;
using SimpleModeler.GUI;
using OpenTK.Graphics.OpenGL;
using OpenTK;
using SimpleModeler.GUI.ModelerEventArgs;
using SimpleModeler.Properties;

namespace SimpleModeler
{
    /// <summary>
    /// Stellt die abstrakte Basisklasse für einen Modelierer dar.
    /// </summary>
    public abstract class AbstractModeler : IModeler, IDisposable
    {
        #region Fields
        private int _lines = 20;
        private int _step = 20;
        private bool _isClosing = false;

        /// <summary>
        /// Caps, welche für das Rendering disabled werden müssen
        /// </summary>
        protected List<EnableCap> _turnoffForPickingMode = new List<EnableCap>();
        /// <summary>
        /// Caps, welche für das Lighting wichtig sind
        /// </summary>
        protected List<EnableCap> _lightCaps = new List<EnableCap>();
        private bool _lastSetupPickingMode = false;
        #endregion // Fields


        #region Indexer
        /// <summary>
        /// Liefert das Objekt mit der angegebenen ID. Sollte kein Objekt mit der ID existieren, so wird null returned
        /// </summary>
        /// <param name="id">ID des Objektes</param>
        /// <returns>Objekt mit der angegebenen ID. Sollte das Objekt nicht existieren, so wird null returned</returns>
        public SceneObject this[ID id]
        {
            get
            {
                if (SceneObjects.ContainsKey(id))
                    return SceneObjects[id];
                return null;
            }
        }

        /// <summary>
        /// Liefert das Objekt mit der angegebenen ID. Sollte kein Objekt mit der ID existieren, so wird null returned
        /// </summary>
        /// <param name="id">ID des Objektes</param>
        /// <returns>Objekt mit der angegebenen ID. Sollte das Objekt nicht existieren, so wird null returned</returns>
        public SceneObject this[int id]
        {
            get { return this[ID.GetIDFromColor(Color.FromArgb(id))]; }
        }
        #endregion // Indexer
        

        #region Properties
        /// <summary>
        /// Liste aller Objekte in der Szene
        /// </summary>
        public Dictionary<ID, SceneObject> SceneObjects { get; protected set; }

        /// <summary>
        /// Das selektierte Objekt
        /// </summary>
        public SceneObject SelectedObject { get; protected set; }

        /// <summary>
        /// Toolbar zum bearbeiten der Objekte
        /// </summary>
        public Toolbar Toolbar { get; private set; }

        /// <summary>
        /// Gibt an, ob Lightning aktiviert ist
        /// </summary>
        private bool UseLightning { get; set; }
        #endregion // Properties


        #region Constructors
        /// <summary>
        /// Konstruktor für Modeler
        /// </summary>
        /// <param name="useLighting">Gibt an, ob Lightning aktiviert werden soll, oder nicht</param>
        public AbstractModeler(bool useLighting)
        {
            this._step = Settings.Default.CoordinateSystemLineStep;
            this._lines = Settings.Default.CoordinateSystemLines;
            this.SceneObjects = new Dictionary<ID, SceneObject>();
            this.UseLightning = useLighting;
            this._turnoffForPickingMode = new List<EnableCap>() { EnableCap.Texture2D };
            this.Toolbar = new Toolbar(this);
            this.Init();
        }
        #endregion // Constructors


        #region Methods
        /// <summary>
        /// Initialisiert den AbstractModeler
        /// </summary>
        protected void Init()
        {
            if (this.UseLightning)
                InitLighting();
            this.Setup(false);

            GL.ClearColor(0.0f, 0.0f, 0.0f, 0.0f);
            GL.ClearDepth(1.0);
            GL.DepthFunc(DepthFunction.Less);
            GL.Enable(EnableCap.DepthTest);
            GL.Enable(EnableCap.LineSmooth);
            GL.ShadeModel(ShadingModel.Smooth);

            //GL.TexEnv(TextureEnvTarget.TextureEnv, TextureEnvParameter.TextureEnvMode, Color.White);
            //GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)TextureMinFilter.Linear);
            //GL.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)TextureMagFilter.Linear);
        }

        /// <summary>
        /// Initialisiert Lighting
        /// </summary>
        protected void InitLighting()
        {
            // Create light components
            float[] ambientLight = { 0.2f, 0.2f, 0.2f, 1.0f };
            float[] diffuseLight = { 0.8f, 0.8f, 0.8f, 1.0f };
            float[] specularLight = { 0.5f, 0.5f, 0.5f, 1.0f };
            float[] position = { -1.5f, 1.0f, -4.0f, 1.0f };

            // Assign created components to GL_LIGHT0
            GL.Light(LightName.Light0, LightParameter.Ambient, ambientLight);
            GL.Light(LightName.Light0, LightParameter.Diffuse, diffuseLight);
            GL.Light(LightName.Light0, LightParameter.Specular, specularLight);
            GL.Light(LightName.Light0, LightParameter.Position, position);

            float[] mcolor = { 1.0f, 0.0f, 0.0f, 1.0f };
            float[] specReflection = { 0.8f, 0.8f, 0.8f, 1.0f };
            GL.Material(MaterialFace.Front, MaterialParameter.AmbientAndDiffuse, mcolor);
            //GL.Material(MaterialFace.Front, MaterialParameter.Specular, specReflection);

            GL.ColorMaterial(MaterialFace.Front, ColorMaterialParameter.AmbientAndDiffuse);

            this._lightCaps.Add(EnableCap.Lighting);
            this._lightCaps.Add(EnableCap.ColorMaterial);
            this._lightCaps.Add(EnableCap.Light0);
            this._turnoffForPickingMode.AddRange(this._lightCaps);
        }

        #region Manipulation-Methods
        /// <summary>
        /// Selektiert das Objekt mit der angegebenen ID
        /// </summary>
        /// <param name="id">ID des zu selektierenden Objektes</param>
        public virtual void Select(ID id)
        {
            if (this.SelectedObject != null)
            {
                if (this.SelectedObject.ID == id) return;
                else this.SelectedObject.IsSelected = false;
            }

            if (SceneObjects.ContainsKey(id))
            {
                this.SelectedObject = SceneObjects[id];
                this.SelectedObject.IsSelected = true;
                this.Toolbar.Select(this.SelectedObject, false);
            }
            else
                this.Unselect();
        }

        /// <summary>
        /// Hebt die Selektierung wieder auf
        /// </summary>
        public virtual void Unselect()
        {
            if (SelectedObject == null) return;
            SelectedObject.IsSelected = false;
            SelectedObject = null;
            this.Toolbar.Unselect(false);
        }
        
        /// <summary>
        /// Löscht das Objekt mit der angegebenen ID
        /// </summary>
        /// <param name="id">ID des zu löschenden Objektes</param>
        public virtual void Delete(ID id)
        {
            if (this.SelectedObject.ID.Equals(id))
                this.Unselect();
            if (this.SceneObjects.ContainsKey(id))
                this.SceneObjects.Remove(id);
        }
        
        /// <summary>
        /// Fügt ein neues Objekt der Auflistung hinzu
        /// </summary>
        /// <param name="so">Das hinzuzufügende Objekt</param>
        public virtual void Add(SceneObject so)
        {
            if (!this.SceneObjects.ContainsKey(so.ID))
            {
                this.SceneObjects.Add(so.ID, so);
                this.Select(so.ID);
            }
        }
        
        /// <summary>
        /// Überschreibt das angegebene Objekt
        /// </summary>
        /// <param name="so">Das Objekt, mit welchem das bereits existierende überschrieben werden soll</param>
        public virtual void Edit(SceneObject so) 
        {
            if (this.SceneObjects.ContainsKey(so.ID))
            {
                this.SceneObjects[so.ID] = so;
            }
        }
        #endregion // Manipulation-Methods


        #region Render-Methods
        /// <summary>
        /// Rendert die Szene mit den Objekten
        /// </summary>
        public virtual void Render() { this.Render(false); }

        /// <summary>
        /// Rendert die Szene mit den Objekten
        /// </summary>
        /// <param name="forPickingMode">Wenn true, wird die Szene im Picking-Mode gezeichnet.</param>
        public virtual void Render(bool forPickingMode)
        {
            Setup(forPickingMode);
            if (forPickingMode) this.RenderForPicking();
            else this.RenderFrame();
        }

        /// <summary>
        /// Renders SceneObjects for picking
        /// </summary>
        protected virtual void RenderForPicking()
        {
            foreach (SceneObject so in SceneObjects.Values)
                so.RenderPicking();
        }

        /// <summary>
        /// Called when it is time to render the next frame. Add your rendering code here.
        /// </summary>
        protected virtual void RenderFrame()
        {
            DrawAxes();
            DrawFloor();
            foreach (SceneObject so in SceneObjects.Values)
                so.RenderFrame();
        }
        
        /// <summary>
        /// Zeichnet das Koordinatensystem
        /// </summary>
        protected void DrawAxes()
        {
            GL.PushMatrix();
            
            // X - Axis red
            GL.Begin(BeginMode.Lines);
            GL.Color3(Color.Red);
            GL.Vertex3(-100.0f, 0.0f, 0.0f);
            GL.Vertex3(100.0f, 0.0f, 0.0f);
            GL.End();
            
            // Y - Axis green
            GL.Begin(BeginMode.Lines);
            GL.Color3(Color.Green);
            GL.Vertex3(0.0f, -100.0f, 0.0f);
            GL.Vertex3(0.0f, 100.0f, 0.0f);
            GL.End();
            
            // Z - Axis blue
            GL.Begin(BeginMode.Lines);
            GL.Color3(Color.Blue);
            GL.Vertex3(0.0f, 0.0f, 100.0f);
            GL.Vertex3(0.0f, 0.0f, -100.0f);
            GL.End();

            GL.PopMatrix();
        }

        /// <summary>
        /// Zeichnet den Boden der Szene
        /// </summary>
        protected void DrawFloor()
        {
            GL.Color3(Color.LightGray);

            GL.PushMatrix();
            GL.Translate(new Vector3(0, 0, -(_lines * _step)));
            for (int x = -_lines; x < _lines - 1; x++)
            {
                GL.Translate(new Vector3(0, 0, _step));
                if (x == -1) continue;

                GL.Begin(BeginMode.Lines);
                GL.Vertex3(-100.0f, 0.0f, 0.0f);
                GL.Vertex3(100.0f, 0.0f, 0.0f);
                GL.End();
            }
            GL.PopMatrix();

            GL.PushMatrix();
            GL.Translate(new Vector3(-(_lines * _step), 0, 0));
            for (int z = -_lines; z < _lines - 1; z++)
            {
                GL.Translate(new Vector3(_step, 0, 0));
                if (z == -1) continue;

                GL.Begin(BeginMode.Lines);
                GL.Vertex3(0.0f, 0.0f, -100.0f);
                GL.Vertex3(0.0f, 0.0f, 100.0f);
                GL.End();
            }
            GL.PopMatrix();
        }
        #endregion // Render-Methods


        /// <summary>
        /// Setzt diverse Parameter für die Szene. Wird vor jedem Rendering aufgerufen
        /// </summary>
        /// <param name="forPickingMode">Gibt an, ob das Setup für den PickingMode vorgenommen werden soll</param>
        protected void Setup(bool forPickingMode)
        {
            if (_lastSetupPickingMode != forPickingMode)
            {// Wenn der Mode gewechselt wurde
                if (forPickingMode)
                {// Alle caps deaktivieren, welche den pickingMode stören
                    foreach (var cap in this._turnoffForPickingMode)
                    {
                        if (GL.IsEnabled(cap))
                            GL.Disable(cap);
                    }
                }
                else if(this.UseLightning)
                {// Alle für das Rendern wichtigen caps aktivieren
                    foreach (var cap in this._lightCaps)
                    {
                        if(!GL.IsEnabled(cap))
                            GL.Enable(cap);
                    }
                }
            }
            _lastSetupPickingMode = forPickingMode;

            if (forPickingMode) // Color- und Z-Buffer leeren
                GL.Clear(ClearBufferMask.ColorBufferBit | ClearBufferMask.DepthBufferBit);
        }

        /// <summary>
        /// Called when it is time to setup the next frame.
        /// </summary>
        /// <param name="e">Contains timing information.</param>
        public void UpdateFrame(FrameEventArgs e)
        {
            lock (this.Toolbar.Queue.SyncRoot)
            {// Lock auf die Queue um die Events abarbeiten zu können
                while (this.Toolbar.Queue.Count > 0)
                {// Solange Events in der Queue sind, sollen diese abgearbeitet werden
                    var ev = this.Toolbar.Queue.Dequeue();

                    var sc = ev as SceneObjectEventArgs;
                    if (sc != null)
                    {
                        if (sc is EditEventArgs) this.Edit(sc.SceneObject);
                        else if (sc is SelectEventArgs) this.Select(sc.SceneObject.ID);
                    }
                    else
                    {
                        var id = ev as IDEventArgs;
                        if (id != null)
                        {
                            if (id is DeleteEventArgs) this.Delete(id.ID);
                        }
                        else
                        {
                            if (ev is UnselectEventArgs) this.Unselect();
                            if (ev is AddEventArgs)
                            {
                                SceneObject so = (SceneObject)Activator.CreateInstance((ev as AddEventArgs).Type);
                                this.Add(so);
                            }
                        }
                    }
                }
            }
        }
        
        /// <summary>
        /// Zeigt die Toolbar an
        /// </summary>
        public void ViewToolbar() { this.Toolbar.ShowToolbar(); }


        #region IDisposable-Implementation
        /// <summary>
        /// Gibt Ressourcen frei
        /// </summary>
        public void Dispose() { Close(); }

        /// <summary>
        /// Schließt den Modeler
        /// </summary>
        public void Close()
        {
            this._isClosing = true;
            foreach (var sc in this.SceneObjects.Values)
                sc.Dispose();
            this.SceneObjects = null;
            this.Toolbar.Close();
        }
        #endregion // IDisposable-Implementation
        #endregion // Methods
    }
}
